#!/usr/bin/python
# coding:utf-8

import logging
import time
import db
from _dict import Dict
'''
usage:

    from models import User, Blog, Comment
    from feling import db

    db.create_engine(user='root', password='xiaoff', database='webapp_py')

    u = User(name='Test', email='test@example.com', password='1234567890', image='about:blank' )
    u.insert()
    print 'new user id:', u.id

    u1 = User.find_first('where email=?', 'test@example.com')
    print "find user's name:", u1.name

    # u1.delete()

    u2 = User.find_first('where email=?', 'test@example.com')
    print 'find user:', u2



by the way:
    mappings in metaclass will contains all keys defined in class like: User or Blog or Comment

    when you create an instance use User(id = 1, aa = "aa")
    mappings will contains {
                            id = someField, 
                            email = someField, ==> {email.name = 'email'  .default = ''  .ddl = 'text' .nullable = False ...}
                            password = someField, 
                            admin = someField, 
                            name = someField, 
                            image = someField, 
                            created_at = someField
                            }#there are all attributes we defined in class User

    {id = 1,aa='aa'} will in the instance of User, use self.id or self['id'] to get it  

    #as 'db.insert(self.__table__, **params)'
    we put {id = 1, aa = "aa"} into params, 
        aa = 'aa' will be ignore ,because aa is not instance Field ,has no Attribute insertable or updatable
        id = 1, is not insertable or updatable, either.but in mappings id is insertable, 
        so, use 'params[id.name]=getattr(self, id)' which id.name comes from mappings

    of course we need email and password and so on 
        so, when we can't use self.email or self[email] to get email #not email in self
        we use 'self[email] = mappings[email].default', then put email to params #actually,we have copy mappings into attrs[__mappings__]

    now ,you can easily understand insert() update() in the end of this file
    in these two method, we use 'k in self' to judge if self has attribute id ,email,admin ...  
    hasattr(self, k) does not works well,even though we have overrite __getattr__ in 'Model'

'''

class Field(object):

    _count = 0

    def __init__(self, **kw):
        self.name = kw.get('name', None)
        self._default = kw.get('default', None)
        self.primary_key = kw.get('primary_key', False)
        self.nullable = kw.get('nullable',  False)
        self.updatable = kw.get('updatable', True)
        self.insertable = kw.get('insertable', True)
        self.ddl = kw.get('ddl', '')
        self._order = Field._count
        Field._count = Field._count + 1

    @property
    def default(self):
        d = self._default
        return d() if callable(d) else d
        # return d

    def __str__(self):
        s = ['<%s:%s,%s,default(%s),' % (
            self.__class__.__name__, self.name, self.ddl, self._default)]
        self.nullable and s.append('N')
        self.updatable and s.append('U')
        self.insertable and s.append('I')
        s.append('>')
        return ''.join(s)


class StringField(Field):

    def __init__(self, **kw):
        if not 'default' in kw:
            kw['default'] = ''
        if not 'ddl' in kw:
            ke['ddl'] = 'varchar(255)'
        super(StringField, self).__init__(**kw)


class IntegerField(Field):

    def __init__(self, **kw):
        if not 'default' in kw:
            '''
            if primary key `id` is aotu_increment default 0 will not effect
            other number will cause: Duplicate entry ... error
            '''
            kw['default'] = 0
        if not 'ddl' in kw:
            kw['ddl'] = 'bigint'
        super(IntegerField, self).__init__(**kw)


class FloatField(Field):

    def __init__(self, **kw):
        if not 'default' in kw:
            kw['default'] = 0.0
        if not 'ddl' in kw:
            kw['ddl'] = 'real'
        super(FloatField, self).__init__(**kw)


class BooleanField(Field):

    def __init__(self, **kw):
        if not 'default' in kw:
            kw['default'] = False
        if not 'ddl' in kw:
            kw['ddl'] = 'bool'
        super(BooleanField, self).__init__(**kw)


class TextField(Field):

    def __init__(self, **kw):
        if not 'default' in kw:
            kw['default'] = ''
        if not 'ddl' in kw:
            kw['ddl'] = 'text'
        super(TextField, self).__init__(**kw)


class BlobField(Field):

    def __init__(self, **kw):
        if not 'default' in kw:
            kw['default'] = ''
        if not 'ddl' in kw:
            kw['ddl'] = 'Blob'
        super(BlobField, self).__init__(**kw)


class VersionField(Field):

    def __init__(self, name=None):
        super(VersionField, self).__init__(name=name, default=0, ddl='bigint')

_triggers = frozenset(['pre_insert', 'pre_update', 'pre_delete'])


def _gen_sql(table_name, mappings):
    pk = None
    sql = ['-- generating SQL for %s:' %
        table_name, 'create table `%s` (' % table_name]
    for f in sorted(mappings.values(), lambda x, y: cmp(x._order, y._order)):
        if not hasattr(f, 'ddl'):
            raise StandardError('no ddl in field "%s".' % n)
        ddl = f.ddl
        nullable = f.nullable
        if f.primary_key:
            pk = f.name
        sql.append(nullable and ' `%s` %s,' %
                   (f.name, ddl) or ' `%s` %s not null,' % (f.name, ddl))
    sql.append(' primary key(`%s`)' % pk)
    sql.append(');')
    return '\n'.join(sql)


class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):
        if name == 'Model':
            return type.__new__(cls, name, bases, attrs)
        if not hasattr(cls, 'subclasses'):
            cls.subclasses = {}
        if not name in cls.subclasses:
            cls.subclasses[name] = name
        else:
            logging.warning('Redefine class: %s' % name)
        # logging.info('Scan ORMapping %s...' % name)
        mappings = dict()
        primary_key = None
        for k, v in attrs.iteritems():
            if isinstance(v, Field):
                if not v.name:
                    v.name = k
                # logging.info('Found mapping: %s => %s', k, v)

                if v.primary_key:
                    if primary_key:
                        raise TypeError(
                            'Cannot define more than 1 primary key in class: %s' % name)
                    if v.updatable:
                        v.updatable = False
                    if v.nullable:
                        v.nullable = False
                    primary_key = v
                mappings[k] = v
        if not primary_key:
            raise TypeError('Primary key not defined in class: %s' % name)
        for k in mappings.iterkeys():
            attrs.pop(k)
        if not '__table__' in attrs:
            attrs['__table__'] = name.lower()
        attrs['__mappings__'] = mappings
        attrs['__primary_key__'] = primary_key
        attrs['__sql__'] = lambda self: _gen_sql(attrs['__table__'], mappings)
        for trigger in _triggers:
            if not trigger in attrs:
                attrs[trigger] = None
        return type.__new__(cls, name, bases, attrs)


class Model(Dict):
    __metaclass__ = ModelMetaclass

    def __init__(self, **kw):
        super(Model, self).__init__(**kw)

    @classmethod
    def get(cls, pk):
        '''get  by primary_key'''
        d=db.select_one(
            'select * from ?? where ?? = ?', cls.__table__, cls.__primary_key__.name, pk)
        return cls(**d) if d else None

    @classmethod
    def find_first(cls, where, *args):
        '''
        Find by where clause and return one result. If multiple results found,
        only the first one returned. If no result found, return None.
        '''
        d=db.select_one('select * from ?? ??', cls.__table__, where, *args)
        return cls(**d) if d else None

    @classmethod
    def find_all(cls, *arg):
        '''
        Find all and return list.
        '''
        L=db.select('select * from ??', cls.__table__)
        return [cls(**d) for d in L]

    @classmethod
    def find_by(cls, where, *args):
        '''
        Find by where clause and return list.
        '''
        L=db.select('select * from `%s` %s' % (cls.__table__, where), *args)
        return [cls(**d) for d in L]

    @classmethod
    def count_all(cls):
        '''
        Find by 'select count(pk) from table' and return integer.
        '''
        return db.select_int('select count(`%s`) from `%s`' % (cls.__primary_key__.name, cls.__table__))

    @classmethod
    def count_by(cls, where, *args):
        '''
        Find by 'select count(pk) from table where ... ' and return int.
        '''
        return db.select_int('select count(`%s`) from `%s` %s' % (cls.__primary_key__.name, cls.__table__, where), *args)

    def delete(self):
        self.pre_delete and self.pre_delete()
        pk=self.__primary_key__.name
        args=(getattr(self, pk), )
        db.update('delete from ?? where ?? = ?', self.__table__, pk, *args)
        return self

    def insert(self):
        self.pre_insert and self.pre_insert()
        params={}
        for k, v in self.__mappings__.iteritems():
            if v.insertable:
                if not k in self:
                    setattr(self, k, v.default)
                params[v.name]=getattr(self, k)
        db.insert(self.__table__, **params)
        return self

    def update(self):
        self.pre_update and self.pre_update()
        L=[]
        args=[]
        for k, v in self.__mappings__.iteritems():
            if v.updatable:
                if k in self:
                    arg=getattr(self, k)
                else:
                    arg=v.default
                    setattr(self, k, arg)
                L.append('`%s`=?' % k)
                args.append(arg)
        pk=self.__primary_key__.name
        args.append(getattr(self, pk))
        db.update('update `%s` set %s where %s=?' %
                  (self.__table__, ','.join(L), pk), *args)
        return self
